Flutter TextEditingController – Detailed Notes
TextEditingController is a Flutter class used to control and access the text entered into a TextField or TextFormField. It allows developers to read, modify, clear, and listen to changes in text input. Flutter's official documentation also recommends disposing of a controller when it is no longer needed. :contentReference[oaicite:0]{index=0}
1. What is TextEditingController?
A TextEditingController acts as a connection between your Dart code and a text input widget. Instead of only displaying a text field, the controller allows the application to programmatically interact with the text entered by the user.
It can be used to:
- Read text entered by the user.
- Set initial text.
- Change text programmatically.
- Clear the text field.
- Listen for text changes.
- Access the current selection.
- Access the complete
TextEditingValue.
- Control text fields in login, registration, search, and other forms.
Flutter's API describes TextEditingController as a controller for an editable text field. It extends ValueNotifier, so listeners can react when its value changes. :contentReference[oaicite:1]{index=1}
2. Basic Syntax
final controller = TextEditingController();
Connect it to a TextField:
TextField(
controller: controller,
)
After connecting the controller, the entered text can be retrieved using:
String text = controller.text;
3. Importing Flutter Material
For a Material Flutter application, import the Material package:
import 'package:flutter/material.dart';
4. Creating a TextEditingController
A controller is normally created as a field of a StatefulWidget when the controller needs to live for the lifetime of that widget.
class NamePage extends StatefulWidget {
const NamePage({super.key});
@override
State createState() => _NamePageState();
}
class _NamePageState extends State {
final nameController = TextEditingController();
@override
void dispose() {
nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: nameController,
);
}
}
5. Why Do We Need TextEditingController?
Without a controller, a TextField can still accept user input, but your code does not have a convenient controller object for reading or modifying that value later.
For example, this TextField accepts input:
TextField(
decoration: InputDecoration(
labelText: 'Name',
),
)
With a controller, the application can access the entered value:
final nameController = TextEditingController();
TextField(
controller: nameController,
);
print(nameController.text);
This pattern is commonly used when a button needs to retrieve the current input. Flutter's official recipe demonstrates creating a controller, assigning it to the TextField, and reading its text property. :contentReference[oaicite:2]{index=2}
6. Reading Text Using the text Property
The text property returns the current string being edited.
final controller = TextEditingController();
String value = controller.text;
Example
ElevatedButton(
onPressed: () {
print(nameController.text);
},
child: const Text('Get Name'),
)
The current value can also be displayed on the screen:
Text(nameController.text)
7. Complete Example: Read Text
import 'package:flutter/material.dart';
class ReadTextPage extends StatefulWidget {
const ReadTextPage({super.key});
@override
State createState() => _ReadTextPageState();
}
class _ReadTextPageState extends State {
final nameController = TextEditingController();
@override
void dispose() {
nameController.dispose();
super.dispose();
}
void showName() {
final name = nameController.text;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Name: $name'),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Read Text'),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Enter Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: showName,
child: const Text('Show Name'),
),
],
),
),
);
}
}
8. Setting Initial Text
You can provide initial text when creating the controller.
final nameController = TextEditingController(
text: 'John Doe',
);
Use the controller with a TextField:
TextField(
controller: nameController,
)
Flutter's TextEditingController(String? text) constructor supports an optional initial text value. :contentReference[oaicite:3]{index=3}
9. Changing Text Programmatically
You can assign a new value to the controller's text property.
nameController.text = 'Rahul Sharma';
The connected TextField will display the updated value.
Example
ElevatedButton(
onPressed: () {
nameController.text = 'John Doe';
},
child: const Text('Set Name'),
)
When the text property is set, the controller notifies its listeners and updates the associated text field. The API notes that directly setting text also resets the selection/composing state, so more advanced updates can use the controller's value property. :contentReference[oaicite:4]{index=4}
10. Clearing Text
The clear() method removes all text from the controller.
nameController.clear();
Clear Button Example
IconButton(
onPressed: () {
nameController.clear();
},
icon: const Icon(Icons.clear),
)
The clear() method is useful for search fields, reset buttons, and form reset functionality.
11. Creating a Search Field with Clear Button
class SearchPage extends StatefulWidget {
const SearchPage({super.key});
@override
State createState() => _SearchPageState();
}
class _SearchPageState extends State {
final searchController = TextEditingController();
@override
void dispose() {
searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Search'),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: TextField(
controller: searchController,
decoration: InputDecoration(
labelText: 'Search',
prefixIcon: const Icon(Icons.search),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
searchController.clear();
},
),
border: const OutlineInputBorder(),
),
),
),
);
}
}
12. Listening to Text Changes
A TextEditingController can notify listeners whenever its value changes. Use the addListener() method to register a callback. Flutter's official documentation demonstrates this approach for reacting to changes in a text field. :contentReference[oaicite:5]{index=5}
controller.addListener(() {
print(controller.text);
});
13. Using addListener()
The listener can be attached inside initState().
@override
void initState() {
super.initState();
nameController.addListener(() {
print(nameController.text);
});
}
The controller should be disposed when the widget is removed:
@override
void dispose() {
nameController.dispose();
super.dispose();
}
14. Complete Listener Example
class ListenerExample extends StatefulWidget {
const ListenerExample({super.key});
@override
State createState() => _ListenerExampleState();
}
class _ListenerExampleState extends State {
final controller = TextEditingController();
@override
void initState() {
super.initState();
controller.addListener(_handleTextChange);
}
void _handleTextChange() {
print('Current text: ${controller.text}');
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
decoration: const InputDecoration(
labelText: 'Enter Text',
border: OutlineInputBorder(),
),
);
}
}
15. TextEditingController vs onChanged
Both onChanged and TextEditingController can be used to respond to text changes. Flutter's documentation describes onChanged as the simpler approach, while a controller provides more control and can also read, modify, clear, and observe the field's value. :contentReference[oaicite:6]{index=6}
| Feature | onChanged | TextEditingController |
|---|
| Detect text changes | Yes | Yes |
| Read current text | Callback provides it | controller.text |
| Set text programmatically | No direct controller access | Yes |
| Clear text | No | Yes |
| Listen from multiple places | Limited | Yes |
| Access selection | No direct access | Yes |
| Control field value | Limited | Yes |
16. Accessing Selection
TextEditingController also exposes the current text selection through the selection property.
TextSelection selection = controller.selection;
This is useful when an application needs to know which portion of the text is currently selected.
17. Selecting All Text
You can select all text using the controller's selection property.
controller.selection = TextSelection(
baseOffset: 0,
extentOffset: controller.text.length,
);
For example:
ElevatedButton(
onPressed: () {
controller.selection = TextSelection(
baseOffset: 0,
extentOffset: controller.text.length,
);
},
child: const Text('Select All'),
)
18. Moving the Cursor
You can position the cursor programmatically by changing the selection.
controller.selection = TextSelection.collapsed(
offset: controller.text.length,
);
This places the cursor at the end of the current text.
19. Understanding TextEditingValue
TextEditingController stores a TextEditingValue. This value contains more information than just the text, including the current text and selection.
TextEditingValue currentValue = controller.value;
The controller's value property provides the complete current editing state. :contentReference[oaicite:7]{index=7}
20. Updating the value Property
For advanced use cases, you can update the complete editing value.
controller.value = TextEditingValue(
text: 'Hello Flutter',
selection: TextSelection.collapsed(
offset: 13,
),
);
This approach is useful when you need to update text and selection together.
21. Text Property vs Value Property
| Property | Purpose |
|---|
controller.text | Gets or sets the current text. |
controller.value | Gets or sets the complete TextEditingValue. |
controller.selection | Gets or sets the current selection. |
22. Disposing TextEditingController
When a controller is created by a StatefulWidget, it should be disposed in the dispose() method. This releases resources used by the controller. Flutter's official documentation explicitly recommends calling dispose() when the controller is no longer needed. :contentReference[oaicite:8]{index=8}
@override
void dispose() {
controller.dispose();
super.dispose();
}
Complete Lifecycle
class MyPage extends StatefulWidget {
const MyPage({super.key});
@override
State createState() => _MyPageState();
}
class _MyPageState extends State {
final controller = TextEditingController();
@override
void initState() {
super.initState();
controller.addListener(_listener);
}
void _listener() {
print(controller.text);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
);
}
}
23. TextEditingController with TextField
final nameController = TextEditingController();
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Full Name',
border: OutlineInputBorder(),
),
)
The relationship is:
User Input
↓
TextField
↓
TextEditingController
↓
controller.text
↓
Application Logic
24. TextEditingController with TextFormField
A controller can also be supplied to TextFormField.
final emailController = TextEditingController();
TextFormField(
controller: emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
)
TextFormField integrates a TextField with a surrounding Form, making it useful when validation and form state are required. :contentReference[oaicite:9]{index=9}
25. TextEditingController with Form Validation
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State createState() => _LoginFormState();
}
class _LoginFormState extends State {
final formKey = GlobalKey();
final emailController = TextEditingController();
@override
void dispose() {
emailController.dispose();
super.dispose();
}
void submitForm() {
if (formKey.currentState!.validate()) {
print('Email: ${emailController.text}');
}
}
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: emailController,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter your email';
}
return null;
},
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: submitForm,
child: const Text('Submit'),
),
],
),
);
}
}
26. Multiple TextEditingControllers
Real-world forms often require multiple controllers.
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final passwordController = TextEditingController();
Connect them to individual fields:
TextField(
controller: nameController,
)
TextField(
controller: emailController,
)
TextField(
controller: phoneController,
)
TextField(
controller: passwordController,
obscureText: true,
)
27. Complete Registration Example
import 'package:flutter/material.dart';
class RegistrationPage extends StatefulWidget {
const RegistrationPage({super.key});
@override
State createState() => _RegistrationPageState();
}
class _RegistrationPageState extends State {
final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final passwordController = TextEditingController();
@override
void dispose() {
nameController.dispose();
emailController.dispose();
phoneController.dispose();
passwordController.dispose();
super.dispose();
}
void registerUser() {
final name = nameController.text.trim();
final email = emailController.text.trim();
final phone = phoneController.text.trim();
final password = passwordController.text;
if (name.isEmpty ||
email.isEmpty ||
phone.isEmpty ||
password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please fill all fields'),
),
);
return;
}
print('Name: $name');
print('Email: $email');
print('Phone: $phone');
print('Password: $password');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Registration'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextField(
controller: nameController,
textCapitalization: TextCapitalization.words,
decoration: const InputDecoration(
labelText: 'Full Name',
prefixIcon: Icon(Icons.person),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: phoneController,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'Phone',
prefixIcon: Icon(Icons.phone),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: registerUser,
child: const Text('Register'),
),
),
],
),
),
);
}
}
28. Clearing Multiple Controllers
You can clear all fields when the user taps a Reset button.
void clearForm() {
nameController.clear();
emailController.clear();
phoneController.clear();
passwordController.clear();
}
Use it with a button:
ElevatedButton(
onPressed: clearForm,
child: const Text('Reset'),
)
29. Controller with Search and Live Results
A controller is useful when the application needs to read the search value and perform an action whenever the value changes.
class SearchExample extends StatefulWidget {
const SearchExample({super.key});
@override
State createState() => _SearchExampleState();
}
class _SearchExampleState extends State {
final searchController = TextEditingController();
String searchText = '';
@override
void initState() {
super.initState();
searchController.addListener(updateSearch);
}
void updateSearch() {
setState(() {
searchText = searchController.text;
});
}
@override
void dispose() {
searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: searchController,
decoration: const InputDecoration(
labelText: 'Search',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
Text('Searching for: $searchText'),
],
);
}
}
Flutter's official documentation describes this listener-based approach as one way to detect changes using a TextEditingController. :contentReference[oaicite:10]{index=10}
30. Avoiding Listener Loops
Be careful when modifying controller.text or controller.value from inside a controller listener. Because changing the controller can notify listeners again, careless code can create repeated or infinite update loops. Flutter's API documentation specifically warns about this behavior. :contentReference[oaicite:11]{index=11}
For example, avoid unnecessarily doing this inside a listener:
controller.addListener(() {
controller.text = controller.text.toUpperCase();
});
A safer approach for transformations is often to use input formatters or carefully manage the controller's value and selection.
31. Using TextEditingController with Input Formatting
For input transformations or restrictions, TextInputFormatter is often more appropriate than changing the controller from inside its own listener.
import 'package:flutter/services.dart';
TextField(
controller: phoneController,
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
],
decoration: const InputDecoration(
labelText: 'Phone Number',
border: OutlineInputBorder(),
),
)
32. TextEditingController for API Data
Controllers are also useful when existing application data needs to be displayed in editable fields.
Example
final nameController = TextEditingController();
void loadUser() {
nameController.text = 'John Doe';
}
This pattern can be used for profile-editing screens where information retrieved from an API or database needs to be displayed in TextFields.
33. Editing an Existing Profile
class EditProfilePage extends StatefulWidget {
const EditProfilePage({super.key});
@override
State createState() => _EditProfilePageState();
}
class _EditProfilePageState extends State {
final nameController = TextEditingController(
text: 'John Doe',
);
final emailController = TextEditingController(
text: '[email protected]',
);
@override
void dispose() {
nameController.dispose();
emailController.dispose();
super.dispose();
}
void saveProfile() {
print('Name: ${nameController.text}');
print('Email: ${emailController.text}');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Edit Profile'),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextField(
controller: nameController,
decoration: const InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: saveProfile,
child: const Text('Save Profile'),
),
],
),
),
);
}
}
34. TextEditingController Methods and Properties
| Property/Method | Purpose |
|---|
text | Gets or sets the current text. |
value | Gets or sets the complete TextEditingValue. |
selection | Gets or sets the selected text range. |
clear() | Removes all text. |
addListener() | Registers a listener for changes. |
removeListener() | Removes a previously registered listener. |
dispose() | Releases controller resources. |
hasListeners | Indicates whether listeners are registered. |
These properties and methods are part of the Flutter TextEditingController API. :contentReference[oaicite:12]{index=12}
35. TextEditingController Lifecycle
A typical controller lifecycle in a StatefulWidget looks like this:
- Create the controller.
- Connect it to the TextField.
- Optionally add listeners.
- Read or modify its value during the widget's lifetime.
- Dispose the controller when the widget is removed.
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State createState() => _MyWidgetState();
}
class _MyWidgetState extends State {
final controller = TextEditingController();
@override
void initState() {
super.initState();
controller.addListener(_onTextChanged);
}
void _onTextChanged() {
print(controller.text);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: controller,
);
}
}
36. Common Mistakes
Mistake 1: Not Disposing the Controller
Always dispose controllers that your StatefulWidget owns.
@override
void dispose() {
controller.dispose();
super.dispose();
}
Mistake 2: Creating Controllers Inside build()
Avoid creating a new controller every time the build() method executes.
Incorrect:
Widget build(BuildContext context) {
final controller = TextEditingController();
return TextField(controller: controller);
}
Instead, create the controller as a State field:
final controller = TextEditingController();
Mistake 3: Forgetting to Connect the Controller
Creating a controller alone does not connect it to a TextField.
TextField(
controller: controller,
)
Mistake 4: Using a Listener Without Considering Its Lifecycle
If you register listeners, make sure the controller itself is properly managed and disposed with the widget.
Mistake 5: Modifying the Controller Carelessly Inside Its Listener
Changing the controller from its own listener can trigger another notification and may create an update loop. :contentReference[oaicite:13]{index=13}
37. TextEditingController vs GlobalKey Form
| Concept | Purpose |
|---|
TextEditingController | Read and control the value of an individual text field. |
GlobalKey | Access and validate/save/reset a Form. |
TextFormField | Create a text input field that integrates with Form. |
Form | Group and manage multiple form fields. |
Flutter's Form API provides methods for saving, resetting, and validating form fields, while the TextEditingController focuses on controlling an individual text field's editing value. :contentReference[oaicite:14]{index=14}
38. Practical Login Example
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State createState() => _LoginPageState();
}
class _LoginPageState extends State {
final emailController = TextEditingController();
final passwordController = TextEditingController();
@override
void dispose() {
emailController.dispose();
passwordController.dispose();
super.dispose();
}
void login() {
final email = emailController.text.trim();
final password = passwordController.text;
if (email.isEmpty || password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please enter email and password'),
),
);
return;
}
print('Email: $email');
print('Password: $password');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Login'),
),
body: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextField(
controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: 'Email',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
prefixIcon: Icon(Icons.lock),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: login,
child: const Text('Login'),
),
),
],
),
),
);
}
}
39. Practical Exercise
Create a Flutter profile-editing application using TextEditingController.
Required Fields
- Full Name
- Email
- Phone Number
- Address
- About Me
Requirements
- Create a separate TextEditingController for each field.
- Display initial user information in the fields.
- Allow the user to edit the information.
- Add a Save button.
- Read all values using
controller.text.
- Add a Reset button using
controller.clear() or appropriate initial values.
- Dispose all controllers properly.
40. Interview Questions
- What is TextEditingController in Flutter?
- Why do we use TextEditingController?
- How do you retrieve text from a TextField using a controller?
- What is the purpose of the
text property?
- How do you set initial text in a TextField?
- How do you change TextField text programmatically?
- How do you clear a TextField?
- What is the purpose of
addListener()?
- Why should TextEditingController be disposed?
- Where should a controller normally be created in a StatefulWidget?
- What is the difference between
onChanged and TextEditingController?
- What is
TextEditingValue?
- What is the purpose of the
selection property?
- How can you move the cursor to the end of the text?
- Can TextEditingController be used with TextFormField?
- How can multiple controllers be used in a registration form?
- What happens if a controller is modified inside its own listener?
- How can TextEditingController be used in a search field?
- How can a controller be used to populate an edit-profile form?
- What is the difference between TextEditingController and GlobalKey?
41. Quick Revision
| Task | Code |
|---|
| Create controller | TextEditingController() |
| Connect to TextField | controller: myController |
| Read text | myController.text |
| Set text | myController.text = 'Hello' |
| Clear text | myController.clear() |
| Add listener | myController.addListener(...) |
| Read selection | myController.selection |
| Read complete value | myController.value |
| Dispose | myController.dispose() |
| Initial text | TextEditingController(text: 'Hello') |
42. Key Takeaways
TextEditingController provides programmatic control over editable text.
- Connect the controller to a
TextField using the controller property.
- Use
controller.text to retrieve the current input.
- Use
controller.text = '...' to set text programmatically.
- Use
controller.clear() to remove the current text.
- Use
addListener() to respond to controller changes.
- Use
selection to work with the current cursor or selected text.
- Use
value when you need to work with the complete editing state.
- Dispose controllers when they are no longer needed.
- Controllers can be used with both
TextField and TextFormField.
- For form validation, combine
TextFormField, Form, and appropriate validation logic.
43. Official Flutter Resources
44. JustAcademy Flutter Resources